1 /** 2 The following examples come from 3 $(LINK http://zetcode.com/db/sqlite/constraints/). 4 Even though it is a SQLite tutorial the point is to show how to use this package 5 which does not have to be just SQLite. 6 */ 7 module test.examples_foreign_key; 8 9 version(D_Ddoc) 10 { 11 /// 12 class BlankClassSoDocsWillBeGenerated { } 13 } 14 15 16 /** 17 This example is for the FOREIGN KEY constraints. The tables 18 in SQL can be created by 19 $(D $(D $(D sql 20 CREATE TABLE Authors 21 ( 22 AuthorId INTEGER NOT NULL PRIMARY KEY, 23 Name TEXT 24 ); 25 26 CREATE TABLE Books 27 ( 28 BookId INTEGER NOT NULL PRIMARY KEY, 29 Title TEXT, 30 AuthorId INTEGER, 31 FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId) 32 ); 33 34 ))) 35 36 The Books table now must have values in Books.AuthorId that are in 37 Authors.AuthorId. By default if Authors deletes or updates its AuthorId 38 and Books references the AuthorId, an exception is thrown. If you would 39 like Books to cascade the effects instead you would create the Books table like 40 $(D $(D $(D sql 41 CREATE TABLE Books 42 ( 43 BookId INTEGER NOT NULL PRIMARY KEY, 44 Title TEXT, 45 AuthorId INTEGER, 46 FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId) 47 ON DELETE CASCADE 48 ON UPDATE CASCADE 49 ); 50 51 ))) 52 53 This package can do any of the update or delete rules. 54 For more rules information look at $(WIKI constraints, Rule). 55 56 Below I will create the classes. I will use the cascaded Books for this example. 57 */ 58 unittest 59 { 60 import db_constraints; 61 62 class Author 63 { 64 private int _AuthorId; 65 @PrimaryKeyColumn @NotNull 66 @property int AuthorId() 67 { 68 return _AuthorId; 69 } 70 @property void AuthorId(int value) 71 { 72 setter(_AuthorId, value); 73 } 74 private string _Name; 75 @property string Name() 76 { 77 return _Name; 78 } 79 @property void Name(string value) 80 { 81 setter(_Name, value); 82 } 83 this(int AuthorId_, string Name_) 84 { 85 this._AuthorId = AuthorId_; 86 this._Name = Name_; 87 initializeKeyedItem(); 88 } 89 90 // we must define dup() since we are going 91 // to change AuthorId which is our implied 92 // clustered index 93 Author dup() 94 { 95 return new Author(this._AuthorId, this._Name); 96 } 97 98 mixin KeyedItem!(); 99 } 100 class Authors 101 { 102 mixin KeyedCollection!(Author); 103 } 104 105 // adding in this function so we can get multiple 106 // author records at once 107 Authors GetAuthorsFromDB() 108 { 109 return new Authors([ 110 new Author(1, "Jane Austen"), 111 new Author(2, "Leo Tolstoy"), 112 new Author(3, "Joseph Heller"), 113 new Author(4, "Charles Dickens") 114 ]); 115 } 116 117 118 // we could put Books in a different file 119 // to do that you would just need to import the file where Authors is. 120 121 // attach the foreign key constraint attribute to the singular class 122 @ForeignKeyConstraint!( 123 ["AuthorId"], /* Book column */ 124 "Authors", /* referenced table which is Authors in this case */ 125 ["AuthorId"], /* referenced column which is Authors.AuthorId */ 126 Rule.cascade, /* what to do when we update Authors.AuthorId */ 127 Rule.cascade) /* what to do when we delete Authors.AuthorId */ 128 class Book 129 { 130 private int _BookId; 131 @PrimaryKeyColumn @NotNull 132 @property int BookId() 133 { 134 return _BookId; 135 } 136 @property void BookId(int value) 137 { 138 setter(_BookId, value); 139 } 140 private string _Title; 141 @property string Title() 142 { 143 return _Title; 144 } 145 @property void Title(string value) 146 { 147 setter(_Title, value); 148 } 149 private int _AuthorId; 150 @property int AuthorId() 151 { 152 return _AuthorId; 153 } 154 @property void AuthorId(int value) 155 { 156 setter(_AuthorId, value); 157 } 158 this(int BookId_, string Title_, int AuthorId_) 159 { 160 this._BookId = BookId_; 161 this._Title = Title_; 162 this._AuthorId = AuthorId_; 163 initializeKeyedItem(); 164 } 165 166 mixin KeyedItem!(); 167 } 168 169 class Books 170 { 171 mixin KeyedCollection!(Book); 172 } 173 Books GetBooksFromDB() 174 { 175 return new Books([ 176 new Book(1, "Emma", 1), 177 new Book(2, "War and Peace", 2), 178 new Book(3, "Catch XII", 3), 179 new Book(4, "David Copperfield", 4), 180 new Book(5, "Good as Gold", 3), 181 new Book(6, "Anna Karenia", 2) 182 ]); 183 } 184 185 186 // we will get both collections 187 // and then associate authors to books 188 189 // ON UPDATE CASCADE 190 { 191 auto authors = GetAuthorsFromDB(); 192 auto books = GetBooksFromDB(); 193 194 // when we associate authors to books 195 // there should be no exceptions since 196 // we are starting with correct data 197 import std.exception : assertNotThrown, assertThrown; 198 assertNotThrown!ForeignKeyException(books.authors = authors); 199 // books.authors is a write-only property made by 200 // mixin KeyedCollection!(Book); 201 // you can also set books.authors = null when you want to 202 // remove the association 203 204 // if you recall from before we can use the primary key to 205 // search our collections easily. In Books, Emma has BookId 1. 206 assert(books[1].Title == "Emma"); 207 assert(books[1].BookId == 1); 208 209 // Emma is written by Jane Austen and 210 // in authors Jane Austen has AuthorId 1 211 assert(authors[1].Name == "Jane Austen"); 212 assert(authors.contains(books[1].AuthorId)); 213 assert(authors[1].AuthorId == books[1].AuthorId); 214 215 // lets say somehow we changed Jane Austen to have AuthorId 5 216 // since we have on update cascade for books we would expect 217 // Emma to get AuthorId 5 218 authors[1].AuthorId = 5; 219 // this will also change authors to no longer have key 1 220 assert(authors.contains(5) && !authors.contains(1)); 221 // Emma still has BookId 1 since that did not change but 222 // should have AuthorId 5 223 assert(books[1].Title == "Emma"); 224 assert(books[1].AuthorId == 5); 225 226 // We were able to change Jane Austen's AuthorId since we 227 // defined dup in Author. We did not define dup in Books 228 // which means if we change BookId we should expect a 229 // KeyedException 230 assertThrown!KeyedException(books[1].BookId = 7); 231 232 // it is good but not necessary to set books.authors to null when you 233 // leave scope just to disconnect signals and associations 234 books.authors = null; 235 } 236 237 // ON DELETE CASCADE 238 { 239 auto authors = GetAuthorsFromDB(); 240 auto books = GetBooksFromDB(); 241 books.authors = authors; 242 243 // we have 4 authors 244 assert(authors.length == 4); 245 // and 6 books 246 assert(books.length == 6); 247 248 import std.algorithm : count; 249 // there are two books that have author id 3 250 auto booksWithAuthorId3 = 251 books.byValue.count!((a, b) => a.AuthorId == b)(3); 252 253 assert(booksWithAuthorId3 == 2); 254 255 // this means if we delete AuthorId 3 from authors and 256 // we have on delete cascade for books we should expect 257 // books to have length 4 and authors to have length 3 258 authors.remove(3); 259 assert(authors.length == 3); 260 assert(books.length == 4); 261 262 booksWithAuthorId3 = books.byValue.count!((a, b) => a.AuthorId == b)(3); 263 assert(booksWithAuthorId3 == 0); 264 265 books.authors = null; 266 } 267 268 // ON DELETE RESTRICT 269 { 270 import std.exception : assertThrown, assertNotThrown; 271 auto authors = GetAuthorsFromDB(); 272 auto books = GetBooksFromDB(); 273 books.authors = authors; 274 275 // you can change the on update and on delete rule for your foreign key 276 // by using the foreign key name and _UpdateRule or _DeleteRule...we 277 // did not name ours so it got the default fk_Book_Authors 278 279 // lets say we want to restrict authors deletion now 280 books.fk_Book_Authors_DeleteRule = Rule.restrict; 281 282 // since we have 2 records in books that reference AuthorId 3 we should 283 // get an exception when deleting AuthorId 3 from authors 284 assertThrown!ForeignKeyException(authors.remove(3)); 285 286 // now we can unreference our table and remove 3 without errors 287 books.authors = null; 288 assertNotThrown!ForeignKeyException(authors.remove(3)); 289 290 // but now when we try to re-associate we will get an exception 291 assertThrown!ForeignKeyException(books.authors = authors); 292 } 293 }